package cn.yeziji.forum.dao.impl;

import cn.yeziji.forum.basic.netty.Message;
import cn.yeziji.forum.basic.netty.detail.DetailMessage;
import cn.yeziji.forum.bo.detail.StatisticsDetailsTypeCountBO;
import cn.yeziji.forum.bo.special.DetailSpecialDTO;
import cn.yeziji.forum.common.enums.ForumCommonStatus;
import cn.yeziji.forum.common.ForumPageResult;
import cn.yeziji.forum.common.key.DetailsCacheKey;
import cn.yeziji.forum.common.key.MessageKey;
import cn.yeziji.forum.common.key.NoticeCacheKey;
import cn.yeziji.forum.common.key.RabbitMQConfig;
import cn.yeziji.forum.converter.DetailsConverter;
import cn.yeziji.forum.dao.base.ForumDetailsBaseDao;
import cn.yeziji.forum.dao.intf.ForumDetailsDao;
import cn.yeziji.forum.dao.intf.ForumSpecialColumnDao;
import cn.yeziji.forum.dao.intf.ForumTagsDao;
import cn.yeziji.forum.dao.intf.ForumUserDao;
import cn.yeziji.forum.dao.iquery.DetailQuery;
import cn.yeziji.forum.dto.details.ForumDetailDTO;
import cn.yeziji.forum.dto.details.ForumDetailPagingDTO;
import cn.yeziji.forum.dto.user.ForumUserDTO;
import cn.yeziji.forum.entity.ForumDetailsEntity;
import cn.yeziji.forum.entity.ForumUserEntity;
import cn.yeziji.forum.exception.DetailException;
import cn.yeziji.forum.exception.ForumException;
import cn.yeziji.forum.exception.UserException;
import cn.yeziji.forum.mapper.ForumDetailsMapper;
import cn.yeziji.forum.service.NoticeService;
import cn.yeziji.forum.common.enums.status.DetailsStatus;
import cn.yeziji.forum.common.enums.status.UserStatus;
import cn.yeziji.forum.utils.CacheUtils;
import cn.yeziji.forum.utils.JwtOperaUtils;
import cn.yeziji.forum.utils.ObjectUtils;
import cn.yeziji.forum.utils.TimeUtils;
import cn.yeziji.forum.vo.details.DetailVO;
import cn.yeziji.forum.vo.details.UserStatisticsDetailVO;
import cn.yeziji.forum.vo.user.UserMsgVO;
import com.github.benmanes.caffeine.cache.Cache;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * 文章逻辑实现层
 *
 * @author gzkemays
 * @since 2021/12/21 15:54
 */
@Repository
public class ForumDetailsDaoImpl extends ForumDetailsBaseDao
    implements ForumDetailsDao, NoticeService<DetailVO> {
  @Resource
  RabbitTemplate rabbitTemplate;
  @Resource
  DetailQuery detailQuery;
  @Resource
  HttpServletRequest request;
  @Resource
  ForumTagsDao tagsDao;
  @Resource
  ForumDetailsMapper detailsMapper;
  @Resource
  ForumSpecialColumnDao specialColumnDao;
  @Resource
  ForumUserDao userDao;
  @Resource(name = "detailCache")
  Cache<String, ForumDetailsEntity> detailsCache;
  @Resource
  CacheUtils cache;
  DetailsConverter converter = DetailsConverter.INSTANCE;

  @PostConstruct
  public void init() {
    this.cache = cache.newCaffeineCache(detailsCache).caffeineAddTime(10, TimeUnit.SECONDS);
  }

  @Override
  public int delOrRecovery(ForumDetailDTO dto) {
    if (StringUtils.hasText(dto.getTitle())
        || StringUtils.hasText(dto.getContent())
        || StringUtils.hasText(dto.getDesc())) {
      throw new DetailException(DetailsStatus.DETAILS_OPERATION_FAIL);
    }
    Integer userId = JwtOperaUtils.getUserIdByRequest(request);
    ForumDetailsEntity entity =
        detailsMapper.findOne(detailQuery.delOrRecoveryOneQuery(dto, userId));
    if (entity != null) {
      Boolean delete = dto.getDelete();
      entity.setDelete(delete);
      // 如果是恢复操作，将文章保存为暂存状态。
      if (!delete) {
        entity.setTempSave(true);
      }
      int i = detailsMapper.updateById(entity);
      if (!dto.getTempSave()) {
        cache.remove(DetailsCacheKey.CACHE_PAGE_HASH_KEY);
      }
      cache.blurRemoveHash(DetailsCacheKey.CACHE_PAGE_HASH_KEY, "-(" + userId + ")_");
      return i;
    }
    throw new DetailException(DetailsStatus.DETAILS_IS_NOT_FOUND);
  }

  @Override
  @Transactional(
      rollbackFor = {Exception.class},
      noRollbackFor = {BeanDefinitionValidationException.class},
      isolation = Isolation.REPEATABLE_READ)
  public int save(ForumDetailDTO dto) {
    // 禁止越界操作
    if (dto.getDelete() != null) {
      throw new DetailException(DetailsStatus.DETAILS_OPERATION_FAIL);
    }
    Integer userId = JwtOperaUtils.getUserIdByRequest(request);
    String nickname = JwtOperaUtils.getNickNameByRequest(request);
    Integer id = dto.getId();
    if (userId == null || userId <= 0) {
      throw new UserException(UserStatus.USER_DATA_EXCEPTION);
    }
    // 如果涵盖用户 id 则对其进行赋值
    dto.setUserId(userId);
    dto.setAuthor(nickname);
    // 跟对象进行转换
    ForumDetailsEntity entity = converter.dtoConvertToEntity(dto);
    entity.setUpdateTime(new Date());
    // 保存专栏
    if (org.apache.commons.lang3.StringUtils.isNotBlank(dto.getSpecial())) {
      DetailSpecialDTO specialDTO = new DetailSpecialDTO();
      specialDTO.setName(dto.getSpecial());
      int specialSave = specialColumnDao.save(specialDTO);
      entity.setSpecialId(specialSave);
    }
    if (!entity.getTempSave()) {
      // 计算序列号，如果文章为发布，则进行赋值。
      long serialNumber =
          Long.parseLong(String.valueOf(calculatePublishedSerialNumber(dto.getId())));
      entity.setSerialNumber(serialNumber);
    }
    // 更新操作
    if (id != null && id > 0) {
      // 删除文章缓存
      String oneCache = DetailDaoCacheKey.generateBlurKey(dto);
      cache.blurPrefixRemove(oneCache);
      //      Object obj = cache.get(oneCache);
      //      if (obj != null) {
      //        cache.remove(oneCache);
      //      }

      // 更新数据库
      updateEntityByIds(entity);
      // 更新缓存
      //      saveCache(entity);
      // 如果文章为草稿类型，则判断缓存中是否拥有数据，如果有则需要删除。
      if (entity.getTempSave()) {
        // 如果是将文章从可视变成草稿，则清空浏览数量。
        // FIXME 点赞、收藏、评分等暂且不进行清理
        ForumDetailsEntity one =
            detailsMapper.findOne(detailQuery.getViewAndTempSaveStatusByIdQuery(id));
        Long view = one.getView();
        Boolean tempSave = one.getTempSave();
        if (!tempSave.equals(dto.getTempSave()) && view != null && view != 0) {
          detailsMapper.updateBy(super.updater().set.view().is(0).end());
        }
      }
    } else {
      entity.setId(null);
      detailsMapper.insert(entity);
    }

    // 发布时，删除文章列表缓存并进行通信。
    if (!entity.getTempSave()) {
      cache.remove(DetailsCacheKey.CACHE_PAGE_HASH_KEY);
      notice(converter.entityConvertToDetail(entity));
    }
    // 不管是否为发布都清除用户自己的缓存。
    cache.blurRemoveHash(DetailsCacheKey.CACHE_PAGE_HASH_KEY, "-(" + userId + ")_");
    // 如果标签不为空则进行标签添加
    if (entity.getTags() != null && !entity.getTempSave()) {
      tagsDao.insertByDetail(entity);
    }
    return entity.getId();
  }

  @Override
  public DetailVO getOne(ForumDetailDTO dto) {
    Integer userId = JwtOperaUtils.getUserIdIfZero(request);
    String cacheKey = DetailDaoCacheKey.generateKey(dto, userId);
    DetailVO detailVO = cache.get(cacheKey);
    if (detailVO == null) {
      Optional<Map<String, Object>> oneMap =
          detailsMapper.findOneMap(detailQuery.getOneQuery(dto, userId));
      if (oneMap.isPresent()) {
        Map<String, Object> map = oneMap.get();
        fillPublishTime(map);
        detailVO = converter.mapConvertToDetail(oneMap.get());
        // 加入专栏文章列表
        if (detailVO.getSpecial() != null) {
          Object specialId = map.get("specialId");
          Object authorId = map.get("authorId");
          List<ForumDetailsEntity> entities =
              mapper().listEntity(detailQuery.findSpecialDetailsQuery(specialId, authorId));
          detailVO.setSpecialVOS(converter.entitiesConvertToDetailSpecials(entities));
        }
        cache.set(cacheKey, detailVO, 1, TimeUnit.DAYS);
        return detailVO;
      }
      throw new DetailException(DetailsStatus.DETAILS_IS_NOT_FOUND);
    }
    // 如果浏览的文章为非草稿则更新浏览记录
    if (!dto.getTempSave()) {
      Long view = detailVO.getView();
      detailsMapper.updateBy(
          super.updater().set.view().applyFunc("view+1").end().where.id().eq(dto.getId()).end());
      detailVO.additionView();
      cache.set(cacheKey, detailVO, 30, TimeUnit.MINUTES);
    }
    return detailVO;
  }

  @Override
  public ForumPageResult<DetailVO> getList(ForumDetailPagingDTO dto) {
    Integer userId = checkNickNameToId(dto.getNickname());
    if (dto.getTempSave() != null && dto.getTempSave() && userId == 0) {
      // 如果获取的是暂存的文章，那么就必须要有 userId
      throw new DetailException(DetailsStatus.DETAILS_OPERATION_FAIL);
    }
    String cacheKey = DetailDaoCacheKey.generateKey(dto, userId);
    ForumPageResult<DetailVO> redisHash =
        cache.getByHash(DetailsCacheKey.CACHE_PAGE_HASH_KEY, cacheKey);
    if (redisHash == null) {
      int count = detailsMapper.countNoLimit(detailQuery.getListQuery(dto, userId));
      dto.total(count);
      List<Map<String, Object>> maps =
          detailsMapper.listMaps(detailQuery.getListQuery(dto, userId));
      List<DetailVO> collect =
          maps.stream()
              .map(
                  map -> {
                    fillPublishTime(map);
                    DetailVO vo = converter.mapConvertToDetail(map);
                    // FIXME: 忘了是干嘛的。
                    /*                    if (!ObjectUtils.nonNullAndEqTarget(map.get("uid"), userId)) {
                      vo.defaultOperation();
                    }*/
                    return vo;
                  })
              .collect(Collectors.toList());
      redisHash = new ForumPageResult<>(collect, dto);
      cache.setHash(DetailsCacheKey.CACHE_PAGE_HASH_KEY, cacheKey, redisHash, 10, TimeUnit.MINUTES);
    }
    return redisHash;
  }

  @Override
  public List<DetailVO> getUserDetailList(ForumDetailPagingDTO dto) {
    return getList(dto).getData();
  }

  @Override
  public ForumPageResult<DetailVO> getUserCollectionOrGoodDetails(ForumDetailPagingDTO dto) {
    Integer userId = checkNickNameToId(dto.getNickname());
    String cacheKey = DetailDaoCacheKey.generateKey(dto, userId);
    ForumPageResult<DetailVO> cacheObjs =
        cache.getByHash(DetailsCacheKey.CACHE_PAGE_HASH_KEY, cacheKey);
    if (cacheObjs == null) {
      int count = detailsMapper.countNoLimit(detailQuery.getListTypeQuery(dto, userId));
      dto.total(count);
      cacheObjs =
          new ForumPageResult<>(
              converter.entitiesConvertToDetails(
                  listEntity(detailQuery.getListTypeQuery(dto, userId))),
              dto);
      cache.setHash(DetailsCacheKey.CACHE_PAGE_HASH_KEY, cacheKey, cacheObjs, 10, TimeUnit.MINUTES);
    }
    return cacheObjs;
  }

  @Override
  @SuppressWarnings("unchecked")
  public UserStatisticsDetailVO getStatistics(ForumUserDTO dto) {
    Integer userId = checkNickNameToId(dto.getUsername());
    dto.setUserId(userId);
    UserStatisticsDetailVO cacheVo =
        cache.getByHash(DetailsCacheKey.CACHE_STATISTICS_KEY, userId.toString());
    if (cacheVo == null) {
      List<StatisticsDetailsTypeCountBO> sdtcs =
          listPoJos(
              StatisticsDetailsTypeCountBO.class, detailQuery.getStatisticsTypeCountQuery(dto));
      if (!sdtcs.isEmpty()) {
        AtomicInteger collections = new AtomicInteger(0);
        AtomicInteger goods = new AtomicInteger(0);
        for (StatisticsDetailsTypeCountBO sdtc : sdtcs) {
          if (sdtc.getToCollection()) {
            collections.incrementAndGet();
          }
          if (sdtc.getToGood()) {
            goods.incrementAndGet();
          }
        }
        cacheVo = new UserStatisticsDetailVO(collections.get(), goods.get());
      } else {
        cacheVo = new UserStatisticsDetailVO(0, 0);
      }
      cache.setHash(
          DetailsCacheKey.CACHE_STATISTICS_KEY, userId.toString(), cacheVo, 1, TimeUnit.MINUTES);
    }
    return cacheVo;
  }

  @Override
  public List<ForumDetailsEntity> getUser7DaysDetails(Integer userId) {
    return mapper.listEntity(detailQuery.getUser7DaysDetails(userId));
  }

  /**
   * 组成 key 值
   *
   * @param detailId 文章id
   * @param userId 用户 id
   * @return key
   */
  public String getKey(Integer detailId, Integer userId, boolean isTemp) {
    String prefix = isTemp ? DetailsCacheKey.TEMP_SAVE_KEY : DetailsCacheKey.CACHE_SAVE_KEY;
    return prefix + "_" + detailId + "-" + userId;
  }

  /**
   * 判断是否到更新数据库时间
   *
   * @param expire 剩余时间
   * @return update
   */
  public boolean updateTime(long expire) {
    return expire < TimeUtils.getTime(2, TimeUnit.SECONDS);
  }

  /**
   * 补充 publishTime
   *
   * @param map 查询 map 集合
   */
  public void fillPublishTime(Map<String, Object> map) {
    String format = "yyyy-MM-dd HH:mm:ss";
    SimpleDateFormat sdf = new SimpleDateFormat(format);
    String publishTime =
        map.get("publishTime") == null
            ? sdf.format(map.get("createTime"))
            : sdf.format(map.get("publishTime"));
    Long disparityDay = TimeUtils.getDisparityDay(publishTime, sdf.format(new Date()), format);
    assert disparityDay != null;
    if (disparityDay <= 5) {
      publishTime = disparityDay == 0 ? "今天" : disparityDay + "天前";
    }
    map.put("publishTime", publishTime);
  }

  /**
   * 根据用户名检查用户 id
   *
   * <p>通过 nickname 获得用户 id，如果 nickname 为空那么就从 request 中获取。如果 request 没有 token 或者没有 nickname 那么返回 0。
   *
   * @param nickname 用户昵称
   * @return 用户id
   */
  public Integer checkNickNameToId(String nickname) {
    if (nickname != null) {
      if (!JwtOperaUtils.hasToken(request)) {
        throw new ForumException(ForumCommonStatus.LOGIN_NULL);
      }
      return userDao.selectUserByNickName(nickname).getId();
    } else {
      return JwtOperaUtils.getUserIdIfZero(request);
    }
  }

  /**
   * 计算发布文章的序列号
   *
   * <p>如果没有发布过的文章，则为1，否则为最新发布过的下一个。
   *
   * @param id 文章id
   * @return
   */
  public Long calculatePublishedSerialNumber(Integer id) {
    // 如果有文章 id 传入则意味着更新，那么可以续用原来的序列号。
    if (id != null && id > 0) {
      ForumDetailsEntity idEntity = this.selectById(id);
      Long serialNumber = idEntity.getSerialNumber();
      if (ObjectUtils.nonNullAndEqTarget(serialNumber, 0)) {
        return serialNumber;
      }
    }
    List<Long> published =
        detailsMapper.listPoJos(
            detailQuery.publisedSerialQuery(), (m) -> (Long) m.get(DetailQuery.SERIAL_NUMBER_KEY));
    // 如果为空，那么意味着当前发布的文章为第一篇文章。
    if (CollectionUtils.isEmpty(published)) {
      return 1L;
    }
    // 返回列表中最大的值+1
    return published.stream().max(Comparator.comparing(Long::longValue)).get() + 1;
  }

  @Override
  public void notice(DetailVO vo) {
    DetailMessage detailMessage = new DetailMessage();
    UserMsgVO userMsg = userDao.getUserMsg(vo.getAuthor());
    List<ForumUserEntity> fansUserMsg = userDao.getFansUserMsgById(vo.getUserId());
    detailMessage.setAvatar(userMsg.getAvatar());
    detailMessage.setTitle(vo.getTitle());
    detailMessage.setAuthor(vo.getAuthor());
    detailMessage.setId(vo.getId());
    // 保留数据兜底
    Message message = new Message(MessageKey.DETAIL_NOTICE_FLAG);
    // 粉丝用户账号列表
    List<String> fansNames =
        fansUserMsg.stream()
            .map(ForumUserEntity::getUsername)
            .distinct()
            .collect(Collectors.toList());
    message.setTargets(fansNames);
    message.json(detailMessage);
    for (String fansName : fansNames) {
      // 将 message 保存至未读，从头部加入，最新数据在最上层
      cache.getRedisUtils().listHeaderPushAll(NoticeCacheKey.UN_READ + fansName, message);
    }
    // 通知 rabbitmq
    rabbitTemplate.convertAndSend(
        RabbitMQConfig.NOTICE_MSG_EXCHANGE, RabbitMQConfig.NOTICE_MSG_ROUTING_KEY, message);
  }
}
